在 .NET 使用 Server-Sent-Events(SSE)
TLDR
- SSE 是一種由 Server 單向推送資料至 Client 的技術,適合用於即時更新場景。
- SSE 相比 Polling 更節省資源,相比 WebSocket 則更輕量(僅單向傳輸)。
- Server 端必須將
Content-Type設定為text/event-stream,並將Cache-Control設為no-cache。 - 訊息格式必須以
data: <內容>\n\n結尾,其中兩個換行符號代表一則訊息的結束。 - 使用
EventSource時,若需跨域傳送驗證資訊,需設定withCredentials: true,且 Server 端必須正確配置 CORS。
範例專案
本文的可執行範例:CloudyWing/SseProgressSample。
範例已使用 .NET 10 重寫,包含 CancellationToken 處理與長時間工作的進度回報,建議與本文對照閱讀。
SSE 與其他即時通訊方式的比較
在開發即時通訊功能時,常見的技術選擇如下:
- Polling:Client 定期向 Server 發送請求。缺點是會產生大量無效請求,佔用伺服器資源。
- Server-Sent-Events (SSE):由 Server 單向推送資料至 Client。優點是僅需單一 TCP 連線,能有效降低伺服器負擔並減少頻寬浪費。
- WebSocket:建立雙向連線,雙方可隨時傳送資料。適合需要頻繁雙向互動的場景。
SSE JavaScript 的實作
當需要從前端接收 SSE 串流時,可使用瀏覽器原生的 EventSource API。
SSE 程式碼範例
javascript
const sse = new EventSource('Your API Url');
// 監聽 open 事件,當連接成功時會觸發此事件
sse.addEventListener('open', function (e) {
console.log('SSE connection opened');
});
// 監聽 error 事件,當連接錯誤時會觸發此事件
sse.addEventListener('error', function (e) {
console.log('SSE connection error');
});
// 監聽 message 事件,當接收到訊息時會觸發此事件
sse.addEventListener('message', function (e) {
console.log('SSE message received', e);
const data = JSON.parse(e.data);
const messageElement = document.createElement('div');
messageElement.textContent = data.message;
document.body.appendChild(messageElement);
});
// 監聽自訂的 end 事件
sse.addEventListener('end', function (e) {
console.log('SSE custom end', e);
sse.close();
});withCredentials 屬性
什麼情況下會遇到這個問題:當前端需要跨域存取 SSE 服務,且該服務要求攜帶 Cookie 或認證資訊時。
在建立 EventSource 物件時,可設定 withCredentials 屬性以傳送 CORS 驗證資訊。
javascript
const sse = new EventSource('Your API Url', { withCredentials: true } );WARNING
SSE 使用跨域請求時,Server 端的 Header 也需要進行相應的 CORS 設定。
在 .NET 中實作 SSE Server
SSE Server 必須回傳 text/event-stream 格式,且每條訊息需以空行(兩個換行符號 \n\n)作為結束標記。
以 ASHX (泛型處理常式) 實作
什麼情況下會遇到這個問題:在舊版 ASP.NET Web Forms 專案中需要實現即時推送。
csharp
public class SseHandler : IHttpHandler {
public void ProcessRequest (HttpContext context) {
// 設定 response 的 Content-Type 為 text/event-stream
context.Response.ContentType = "text/event-stream";
context.Response.CacheControl = "no-cache";
// 模擬一個 SSE 事件流,每秒發送一個訊息
int count = 0;
while (count < 10) {
count++;
context.Response.Write("data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
context.Response.Flush();
System.Threading.Thread.Sleep(1000);
}
// 回傳自訂的 end 事件
context.Response.Write("event: end\ndata: {}\n\n");
context.Response.Flush();
context.Response.End();
}
public bool IsReusable {
get {
return false;
}
}
}以 ASP.NET Core Web API 實作
什麼情況下會遇到這個問題:在現代 .NET Core 或 .NET 5+ 環境中開發 API 服務。
csharp
[ApiController]
[Route("[controller]")]
public class SseController : ControllerBase {
[HttpGet]
public async Task GetAsync() {
// 設定 response 的 Content-Type 為 text/event-stream
Response.Headers.Add("Content-Type", "text/event-stream;");
Response.Headers.Add("Cache-Control", "no-cache");
// 模擬一個 SSE 事件流,每秒發送一個訊息
int count = 0;
while (count < 10) {
count++;
await Response.WriteAsync($"data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
await Response.Body.FlushAsync();
await Task.Delay(TimeSpan.FromSeconds(1));
}
// 回傳自訂的 end 事件
await Response.WriteAsync("event: end\ndata: {}\n\n");
await Response.Body.FlushAsync();
}
}異動歷程
- 初版文件建立。
- 修正 withCredentials 的相關內容。
- 補上 GitHub 範例專案連結。